/*
 *
 *
Copyright (c) 1998-2001 Aristar, Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. The end-user documentation included with the redistribution, if any,
must include the following acknowledgment:

"This product includes software developed by Aristar, Inc. (http://www.aristar.com/)."

Alternately, this acknowledgment may appear in the software itself, if and
wherever such third-party acknowledgments normally appear.

4. The names "Dialect" and "Aristar, Inc." must not be used to endorse or
promote products derived from this software without prior written permission.
For written permission, please contact info@aristar.com.

5. Products derived from this software may not be called "Dialect", nor may
"Dialect" appear in their name, without prior written permission of Aristar, Inc.

THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
ARISTAR, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

This software consists of voluntary contributions made by many individuals
on behalf of Aristar, Inc.  For more information on Aristar, Inc.,
please see <http://www.aristar.com/>.

Portions of this software are based upon software freely available and
written by individuals not associated with Aristar, Inc.  Copyrights are
maintained in the relevant source code files.
 *
 *
 */

#include "stdafx.h"

#include "CFileModule.h"
#include "Interpreter.h"

#ifdef CRYPTO_SUPPORT
#	include "Blowfish.h"
#endif

///////////////////////////////////
#if defined(USING_MFC) && defined(_DEBUG)
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
///////////////////////////////////

////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
// Call GuaranteeFilePath(TString filename) before trying to open a new file
// since the open will fail if the directory doesn't exist.  This function
// creates all directories that need to exist in the path.
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////

#if defined(__WINNT__) || defined(__WINCE__)
#define MAKE_DIRECTORY(x)	CreateDirectory((x), NULL)
#else
#include <sys/stat.h>
#define MAKE_DIRECTORY(x)	_tmkdir((x),  \
	S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IXOTH)
#endif

static void _GuaranteeFilePath(TString path)
{
	TString first, last;
	int idx;

	last = path;
	idx = last.ReverseFind(DIR_SEPARATOR);

	if (idx == last.Find(DIR_SEPARATOR)) {
		return;	//We have reached the root (eg. C:\)
	}
	else {
		first = last.Left(idx);
		_GuaranteeFilePath(first);
		MAKE_DIRECTORY((LPCTSTR)first);
	}
}

void GuaranteeFilePath(TString filename)
{
	// This function expects the filename to be part of the path
	// sent to it (eg.  C:\aristar\scripts\scriptname.bas)

	int idx = filename.ReverseFind(DIR_SEPARATOR);
	if (idx > 0) {
		// Only check if not root directory or local directory
		_GuaranteeFilePath(filename.Left(idx + 1));
	}
}

static void SplitFullPath(CString input, CString& directory, CString& filename)
{
	directory.Empty();
	filename.Empty();

	if (!input.IsEmpty()) {
		int pos = input.ReverseFind('\\');
		if (pos >= 0) {
			filename = input.Mid(pos + 1);
			directory = input.Left(pos + 1);	// Leave slash
		}
		else {
			filename = input;
		}
	}
}

////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////

// Called by stVoid garbage collection
void freeFile(void* pVoid) {
	TFile* file = (TFile*)pVoid;
	if (file != NULL) {
		file->Close();
		delete file;
	}
}

CFileImpl::CFileImpl():CClassImpl(VALUE(), _T("file"))
{
	m_MemberSymTab = AllocFrame(31);
	// Anything added here is static (becomes shared by all instances of the class)
	m_MemberSymTab.Set(_T("__type__"), (LPCTSTR)_T("file"));
	m_MemberSymTab.Set(_T("__init__"), AllocFunction(__init__, _T("__core__")));
#pragma message("******************  Temp Here ***************")
	//m_MemberSymTab.Set(_T("__del__"), AllocFunction(__del__, _T("__core__")));
	m_MemberSymTab.Set(_T("load"), AllocFunction(QuickLoad, _T("__core__")));
	m_MemberSymTab.Set(_T("save"), AllocFunction(QuickSave, _T("__core__")));
	m_MemberSymTab.Set(_T("dir"), AllocFunction(dir, _T("__core__")));
	m_MemberSymTab.Set(_T("close"), AllocFunction(close, _T("__core__")));
	m_MemberSymTab.Set(_T("readline"), AllocFunction(readline, _T("__core__")));
	m_MemberSymTab.Set(_T("writeline"), AllocFunction(writeline, _T("__core__")));
	m_MemberSymTab.Set(_T("tab"), AllocFunction(writetab, _T("__core__")));
	m_MemberSymTab.Set(_T("read"), AllocFunction(readfile, _T("__core__")));
	m_MemberSymTab.Set(_T("write"), AllocFunction(write, _T("__core__")));
	m_MemberSymTab.Set(_T("exists"), AllocFunction(exists, _T("__core__")));
	m_MemberSymTab.Set(_T("delete"), AllocFunction(_delete, _T("__core__")));
	m_MemberSymTab.Set(_T("mkdir"), AllocFunction(mkdir, _T("__core__")));
	m_MemberSymTab.Set(_T("rmdir"), AllocFunction(rmdir, _T("__core__")));
	m_MemberSymTab.Set(_T("copy"), AllocFunction(copy, _T("__core__")));
	m_MemberSymTab.Set(_T("move"), AllocFunction(move, _T("__core__")));
	m_MemberSymTab.Set(_T("info"), AllocFunction(file_info, _T("__core__")));
	m_MemberSymTab.Set(_T("seek"), AllocFunction(file_seek, _T("__core__")));
	m_MemberSymTab.Set(_T("FILE_BEGIN"), (int)FILE_BEGIN);
	m_MemberSymTab.Set(_T("FILE_END"), (int)FILE_END);
	m_MemberSymTab.Set(_T("FILE_CURRENT"), (int)FILE_CURRENT);
}

CFileImpl::~CFileImpl(){}

VALUE CFileImpl::__init__(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	open(self, argCount, args, pInterp);
	return VALUE();
}

VALUE CFileImpl::__del__(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));
	// There shouldn't be any args for the destructor
	ASSERT(argCount == 0);

	// This will force garbage collection of #lpFile to occur if it
	// exists, which will call freeFile() to close and dispose
#pragma message("******************  Temp Here ***************")
	//self->Remove(_T("#lpFile"));

	return VALUE();
}

VALUE CFileImpl::open(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	if (argCount != 2) {
		pInterp->SyntaxError(_T("File.open function requires two string args <filename, options>."));
		return AllocNil();
	}

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		file = new TFile();
		self->Set(_T("#lpFile"), AllocVoid(file, freeFile, _T("TFile")));
	}
	else {
		file->Close();	// Let's close this before opening a new file
	}

	if ((args[0].Type() == stString) && (args[1].Type() == stString)) {
		TString modifiers = args[1].String();
		modifiers.MakeLower();

		int opt;
		if (modifiers.Find('r') >= 0) opt = TFile::read;
		else if (modifiers.Find('w') >= 0) opt = TFile::write | TFile::create;
		else if (modifiers.Find('+')>= 0) opt = TFile::append;
		else {
			pInterp->SyntaxError(_T("Missing a read or write file option <r | w | +>"));
			return AllocNil();
		}

		if (modifiers.Find('u') >= 0) opt |= TFile::unicode;
		if (modifiers.Find('a') >= 0) opt |= TFile::ascii;
		if (modifiers.Find('t') >= 0) opt |= TFile::text;
		if (modifiers.Find('b') >= 0) opt |= TFile::binary;

		GuaranteeFilePath(args[0].String());
		file->Open(args[0].String(), opt);
		SetEOF(false, self);
		self->Set(_T("terminator"), AllocProperty(AllocNil(),
			AllocFunction(WriteTerminator, _T("")), file->GetEOLTerminator()));
	}
	else {
		pInterp->SyntaxError(_T("Function requires two string args <filename, options>."));
		return AllocNil();
	}

	return VALUE();
}

VALUE CFileImpl::close(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	VALUE val = self->Get(_T("#lpFile"));
	TFile* file = (TFile*)(val.Void());
	if (file != NULL) {
		file->Close();	// Let's close this before opening a new file
	}
	SetEOF(true, self);
	return VALUE();
}

VALUE CFileImpl::readline(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}

	TString result;
	file->ReadLine(result);
	if (file->IsEOF()) {
		SetEOF(true, self);
	}
	return result;
}

VALUE CFileImpl::writeline(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}
	else if (argCount == 0) {
		file->WriteLine(_T(""));
	}
	else if (argCount == 1) {
		file->WriteLine(args[0].String());
	}
	else {
		pInterp->RuntimeError(_T("File.writeline takes exactly one argument"));
		return AllocNil();
	}
	return true;
}

VALUE CFileImpl::writetab(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}
	else if (argCount == 0) {
		file->Write(_T("\t"));
	}
	else {
		pInterp->RuntimeError(_T("File.tab takes no arguments"));
		return AllocNil();
	}
	return true;
}

VALUE CFileImpl::write(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}
	else if (argCount == 0) {
		pInterp->RuntimeError(_T("File.write requires one argument"));
		return AllocNil();
	}

	if (file->GetFileType() == TFile::binary) {
		if (args[0].Type() != stChunk) {
			pInterp->RuntimeError(_T("Binary file write function can only write chunk data"));
			return AllocNil();
		}
		int length = args[0].GetChunkByteCount();
		file->Write(args[0].GetChunkData(), length);
		return length;
	}
	else {
		TString text = args[0].String();
		file->Write(text);
		return (int)(text.GetLength());
	}
}

VALUE CFileImpl::readfile(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}

	int charsToRead;
	if (argCount > 0) {
		if ((args[0].Type() != stInteger) && (args[0].Type() != stNil)) {
			pInterp->RuntimeError(_T("If present, the arg to file.read must be an integer"));
			return AllocNil();
		}
		charsToRead = args[0].Integer();
	}
	else {
		charsToRead = file->GetLength();
	}

	int count;
	VALUE vResult;
	if (file->GetFileType() == TFile::binary) {
		unsigned char *buf = new unsigned char[charsToRead];
		count = file->Read(buf, charsToRead);
		vResult = AllocChunk(count, buf);
		delete[] buf;
	}
	else {
		TString result;
		count = file->Read(result, charsToRead);
		vResult = result;
	}

	if (count < charsToRead) {
		SetEOF(true, self);
	}
	return vResult;
}

VALUE CFileImpl::WriteTerminator(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	ASSERT(argCount == 2);

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("Can't set terminator.  File has not been opened yet."));
		return AllocNil();
	}

	if (args[1].Type() == stString) {
		switch (args[1].Length()) {
			case 0:
				file->SetEOLTerminator(_T(""));
				return AllocNil();
			case 1:
			case 2:
				file->SetEOLTerminator(args[1].String());
				return args[1];
			default:
				pInterp->RuntimeError(_T("Invalid file.terminator assignment: String more than two characters long"));
		}
	}
	else if ((args[1].Type() == stInteger) && (args[1].Integer() < 256)) {
		TCHAR ch = (TCHAR)(args[1].Integer());
		file->SetEOLTerminator(TString(ch));
		return TString(ch);
	}
	else if (args[1].Type() == stNil) {
		file->SetEOLTerminator(_T(""));
		return args[1];
	}
	else {
		pInterp->SyntaxError(_T("Invalid file.terminator assignment"));
	}
	return args[0];	// Keep old terminator
}

void CFileImpl::SetEOF(bool bEOF, VALUE* self)
{
	self->Set(_T("eof"), bEOF);
}

VALUE CFileImpl::dir(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	bool bOptions = false;
	bool bDirectories = true;
	bool bFiles = true;
	bool bReadonly = true;
	bool bHidden = true;
	bool bFullPath = false;

	if ((argCount != 1) && (argCount != 2)) {
		pInterp->SyntaxError(_T("Invalid parm count to dir()"));
		return AllocNil();
	}
	else if (args[0].Type() != stString) {
		pInterp->SyntaxError(_T("Invalid first arg to dir()"));
		return AllocNil();
	}
	else if (argCount == 2) {
		VALUE v = args[1];
		if (v.Type() != stNil) {
			if (v.Type() != stFrame) {
				pInterp->SyntaxError(_T("Invalid second arg to dir()"));
				return AllocNil();
			}
			bOptions = true;
			bDirectories = v[_T("directories")].Type() != stUndefined ? v[_T("directories")].Boolean() : true;
			bFiles = v[_T("files")].Type() != stUndefined ? v[_T("files")].Boolean() : true;
			bReadonly = v[_T("readonly")].Type() != stUndefined ? v[_T("readonly")].Boolean() : true;
			bHidden = v[_T("hidden")].Type() != stUndefined ? v[_T("hidden")].Boolean() : true;
			bFullPath = v[_T("fullpath")].Type() != stUndefined ? v[_T("fullpath")].Boolean() : false;
		}
	}

	int count = 0;
	int arraySize = 64;
	VALUE keys = AllocArray(arraySize);
	TString searchKey = args[0].String();

	TString dir, f;
	SplitFullPath(searchKey, dir, f);

#if defined(__WINNT__) || defined(__WINCE__)
	WIN32_FIND_DATA wfd;
	HANDLE hFind = FindFirstFile(searchKey, &wfd);
	if (hFind != INVALID_HANDLE_VALUE) {
		do {
			if (count >= arraySize) {
				arraySize += 64;
				keys.Resize(arraySize);
			}

			if ((_tcscmp(_T("."), wfd.cFileName) != 0) && (_tcscmp(_T(".."), wfd.cFileName) != 0)) {
				if (!bOptions) {
					f = wfd.cFileName;
					if (bFullPath) f = dir + f;
					keys.Set(++count, (LPCTSTR)f);
				}
				else if (bDirectories && (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
					if ((bReadonly || !(wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) &&
						(bHidden || !(wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)))
					{
						f = wfd.cFileName;
						if (bFullPath) f = dir + f;
						keys.Set(++count, (LPCTSTR)f);
					}
				}
				else if (bFiles && !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
					if ((bReadonly || !(wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) &&
						(bHidden || !(wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)))
					{
						f = wfd.cFileName;
						if (bFullPath) f = dir + f;
						keys.Set(++count, (LPCTSTR)f);
					}
				}
			}
		} while (FindNextFile(hFind, &wfd));

		FindClose(hFind);
	}
#endif

	keys.Resize(count);
	return keys;
}

static TString AdjustLoadSavePath(TString path)
{
	if (path.Find(DIR_SEPARATOR) == -1) {
		// No path given, default to path of interpreter exe
		TCHAR buf[MAX_PATH];
		GetModuleFileName(NULL, buf, MAX_PATH);
		TString exePath = buf;
		int pos = exePath.ReverseFind(DIR_SEPARATOR);
		if (pos >= 0) {
			path = exePath.Left(pos + 1) + path;
		}
	}
	return path;
}

VALUE CFileImpl::QuickLoad(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount <= 0) || (argCount > 3)) {
		pInterp->SyntaxError(_T("file.load takes one or two params <filename, 'ascii | 'binary | 'unicode>"));
		return AllocNil();
	}

	TString filename = AdjustLoadSavePath(args[0].String());
	if (!TFile::Exists(filename)) {
		pInterp->RuntimeError(_T("File does not exist (or can't be opened): ") + filename);
		return AllocNil();
	}

	int type;
	if (argCount == 1) {
		// Autodetect file type (serial or ascii)
		// Redo this someday so we don't end up opening this file twice...
		TFile testfile;
		if (!testfile.Open(filename, TFile::read)) {
			pInterp->RuntimeError(_T("Failed to open file for reading: %s"),
				(LPCTSTR)filename);
			return AllocNil();
		}
		U8 buf[2] = {0, 0};
		testfile.Read(buf, 2);
		testfile.Close();
		if ((buf[0] == 'd' + 128) && (buf[1] == 's' + 128)) {
			type = 0;	// binary
		}
		else {
			type = 1;	// ASCII
		}
	}
	else if (argCount >= 2) {
		if (args[1].String().CompareNoCase(_T("binary")) == 0) type = 0;
		else if (args[1].String().CompareNoCase(_T("ascii")) == 0) type = 1;
		else if (args[1].String().CompareNoCase(_T("unicode")) == 0) type = 1;
#ifdef CRYPTO_SUPPORT
		else if (args[1].String().CompareNoCase(_T("blowfish")) == 0) type = 2;
#endif // CRYPTO_SUPPORT
		else {
			pInterp->RuntimeError(_T("Unknown file type passed to load(): %s"),
				(LPCTSTR)args[1].__unparse__());
			return AllocNil();
		}
	}

	VALUE vResult = AllocNil();
	if (type == 0) {
		vResult.SerializeIn(filename);
		return vResult;
	}

	TFile file;
	if (!file.Open(filename, TFile::read)) {
		pInterp->RuntimeError(_T("Failed to open file for reading: %s"),
			(LPCTSTR)filename);
		return AllocNil();
	}
	int bufLength = file.GetLength();

	if (type == 1) {
		TCHAR* str = new TCHAR[bufLength + 1];
		file.Read(str, bufLength);
		str[bufLength] = '\0';
		file.Close();

		char* test = (char*)str;
		if ((bufLength >= 2) && ((test[0] == 0) || (test[1] == 0))) {
			// The file is unicode, so if sizeof TCHAR == 1, then
			// convert unicode to ascii first
			if (sizeof(TCHAR) == 1) {
				int newLength = bufLength >> 1;
				TCHAR* temp = new TCHAR[newLength + 1];
				for (int idx = 0; idx < newLength + 1; ++idx) {
					temp[idx] = (TCHAR)(((unsigned short*)str)[idx]);
				}
				delete[] str;
				str = temp;
			}
		}
		else {
			// The file is ascii, so if sizeof TCHAR == 2, then
			// onvert ascii to unicode first
			if (sizeof(TCHAR) == 2) {
				int newLength = bufLength << 1;
				TCHAR* temp = new TCHAR[newLength + 1];
				for (int idx = 0; idx < bufLength + 1; ++idx) {
					((TCHAR*)temp)[idx] = (TCHAR)(((char*)str)[idx]);
				}
				delete[] str;
				str = temp;
			}
		}

		VALUE vContext = AllocFrame();
		vContext.Set(_T("__module__"), filename);	// For error reporting

		if (!pInterp->Evaluate(str, vResult, vContext)) {
			VALUE vExc = pInterp->GetException();
			if (vExc.Type() == stClassInstance) {
				vExc.Set(_T("msg"),
					_T("Evaluation of text failed for file '") + filename +
					_T("' in file.load(): ") + vExc[_T("msg")].String());
			}
			else {
				ASSERT(false);	// This should never happen
			}
		}
		delete[] str;
	}
#ifdef CRYPTO_SUPPORT
	else if (type == 2) {
		// Blowfish encrypted file
		VALUE vKey;
		if (argCount != 3) {
			pInterp->RuntimeError(_T("Blowfish key must be third argument"));
			return AllocNil();
		}
		else if (args[2].Type() == stString) {
			vKey = AllocChunk(args[2].String(), enc_hex, it_ui1);
		}
		else if (args[2].Type() == stChunk) {
			vKey = args[2];
		}
		else {
			pInterp->RuntimeError(_T("Blowfish key must be hex string or chunk"));
			return AllocNil();
		}

		unsigned char* buf = new unsigned char[bufLength];
		file.Read(buf, bufLength);
		file.Close();

		CBlowfish bf(vKey.GetChunkData(), vKey.GetChunkByteCount());
		bf.Decrypt((unsigned long*)buf, bufLength >> 3);
		bool bResult = vResult.SerializeInMemory(buf, bufLength);
		delete[] buf;

		if (!bResult) {
			pInterp->RuntimeError(_T("Failed to unserialize data."));
			return AllocNil();
		}
	}
#endif
	return vResult;
}

VALUE CFileImpl::QuickSave(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if (argCount > 4) {
		pInterp->SyntaxError(_T("file.save takes three args <filename, expression, 'binary | 'ascii>"));
		return AllocNil();
	}
	else {
		TString filename = AdjustLoadSavePath(args[0].String());
		GuaranteeFilePath(filename);
		if (args[2].String().CompareNoCase(_T("binary")) == 0) {
			args[1].SerializeOut(filename);
			return true;
		}
#ifdef CRYPTO_SUPPORT
		else if (args[2].String().CompareNoCase(_T("blowfish")) == 0) {
			VALUE vKey;
			if (argCount != 4) {
				pInterp->RuntimeError(_T("Blowfish key must be fourth argument"));
				return AllocNil();
			}
			else if (args[3].Type() == stString) {
				vKey = AllocChunk(args[3].String(), enc_hex, it_ui1);
			}
			else if (args[3].Type() == stChunk) {
				vKey = args[3];
			}
			else {
				pInterp->RuntimeError(_T("Blowfish key must be hex string or chunk"));
				return AllocNil();
			}
			CBlowfish bf(vKey.GetChunkData(), vKey.GetChunkByteCount());
			VALUE vOutput = args[1].SerializeOutMemory();
			if (vOutput.Type() != stChunk) {
				pInterp->RuntimeError(_T("Failed to serialize value for encrypting"));
				return AllocNil();
			}
			bf.Encrypt((unsigned long*)vOutput.GetChunkData(), vOutput.GetChunkByteCount() >> 3);
			TFile file;
			if (!file.Open(filename, TFile::create | TFile::write | TFile::binary)) {
				pInterp->RuntimeError(_T("Failed to create file"));
				return AllocNil();
			}
			file.Write(vOutput.GetChunkData(), vOutput.GetChunkByteCount());
			file.Close();
		}
#endif
		else if (args[2].String().CompareNoCase(_T("unicode")) == 0) {
			TFile file;
			if (!file.Open(filename, TFile::create | TFile::write | TFile::unicode)) {
				pInterp->RuntimeError(_T("Failed to open file for writing: %s"),
					(LPCTSTR)filename);
				return AllocNil();
			}
			file.Write(args[1].__unparse__());
		}
		else if (args[2].String().CompareNoCase(_T("ascii")) == 0) {
			TFile file;
			if (!file.Open(filename, TFile::create | TFile::write | TFile::ascii)) {
				pInterp->RuntimeError(_T("Failed to open file for writing: %s"),
					(LPCTSTR)filename);
				return AllocNil();
			}
			file.Write(args[1].__unparse__());
		}
		else {
			pInterp->SyntaxError(_T("Invalid third arg to file.save (use 'binary, 'ascii, or 'unicode)"));
			return AllocNil();
		}
		return true;
	}
}

VALUE CFileImpl::exists(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 1) || (args[0].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.exists()"));
		return AllocNil();
	}
	return TFile::Exists(args[0].String());
}

VALUE CFileImpl::_delete(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if (((argCount != 1) && (argCount != 2)) || (args[0].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.exists()"));
		return AllocNil();
	}
	bool bForce = false;
	if (argCount == 2) bForce = args[1].Boolean();
	return TFile::DeleteFile(args[0].String(), bForce);
}

VALUE CFileImpl::move(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 2) || (args[0].Type() != stString) || (args[1].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.move()"));
		return AllocNil();
	}
	return TFile::Move(args[0].String(), args[1].String());
}

VALUE CFileImpl::copy(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 2) || (args[0].Type() != stString) || (args[1].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.copy()"));
		return AllocNil();
	}
	return TFile::Copy(args[0].String(), args[1].String());
}

VALUE CFileImpl::mkdir(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 1) || (args[0].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.mkdir()"));
		return AllocNil();
	}
	return TFile::MakeDir(args[0].String());
}

VALUE CFileImpl::rmdir(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 1) || (args[0].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.mkdir()"));
		return AllocNil();
	}
	return TFile::RemoveDir(args[0].String());
}

static TString STtoString(const FILETIME& ft)
{
	TString date, time;
	SYSTEMTIME st;
	FileTimeToSystemTime(&ft, &st);

	LPTSTR buf = date.GetBuffer(128);
	int count = GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, NULL, buf, 128);
	date.ReleaseBuffer(count > 0 ? count - 1 : 0);

	buf = time.GetBuffer(128);
	count = GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, buf, 128);
	time.ReleaseBuffer(count > 0 ? count - 1 : 0);

	return date + _T(" ") + time;
}

VALUE CFileImpl::file_info(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	if ((argCount != 1) || (args[0].Type() != stString)) {
		pInterp->SyntaxError(_T("Invalid parms to file.info(filename)"));
		return AllocNil();
	}

	TString filename = args[0].String();

	DWORD dw = GetFileAttributes(filename);
	if (dw == -1) return AllocNil();

	HANDLE hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL, OPEN_EXISTING, 0, NULL);

	VALUE v = AllocFrame();
	v.Set(_T("readonly"), dw & FILE_ATTRIBUTE_READONLY ? true : false);
	v.Set(_T("system"), dw & FILE_ATTRIBUTE_SYSTEM ? true : false);
	v.Set(_T("hidden"), dw & FILE_ATTRIBUTE_HIDDEN ? true : false);
	v.Set(_T("archive"), dw & FILE_ATTRIBUTE_ARCHIVE ? true : false);
	v.Set(_T("directory"), dw & FILE_ATTRIBUTE_DIRECTORY ? true : false);
	int size = GetFileSize(hFile, NULL);
	if (size != -1) {
		v.Set(_T("size"), size);
	}
	FILETIME ftCreate, ftLastAccess, ftLastWrite;
	if (GetFileTime(hFile, &ftCreate, &ftLastAccess, &ftLastWrite)) {
		FILETIME ft;
		if (FileTimeToLocalFileTime(&ftCreate, &ft)) {
			v.Set(_T("created"), STtoString(ft));
		}
		if (FileTimeToLocalFileTime(&ftLastAccess, &ft)) {
			v.Set(_T("accessed"), STtoString(ft));
		}
		if (FileTimeToLocalFileTime(&ftLastWrite, &ft)) {
			v.Set(_T("modified"), STtoString(ft));
		}
	}
	CloseHandle(hFile);
	return v;
}

VALUE CFileImpl::file_seek(VALUE* self, int argCount, VALUE args[], CInterpreter* pInterp)
{
	ASSERT_SELF(_T("__core__"));

	TFile* file = (TFile*)(self->Get(_T("#lpFile")).Void());
	if (file == NULL) {
		pInterp->RuntimeError(_T("File has not been opened yet"));
		return AllocNil();
	}

	if ((argCount != 2) || (args[0].Type() != stInteger) || (args[1].Type() != stInteger)) {
		pInterp->RuntimeError(_T("Invalid args to file.seek"));
		return AllocNil();
	}

	long offset = args[0].Integer();
	if (file->GetFileType() == TFile::unicode) {
		offset = offset * 2;
	}
	return (int)file->Seek(offset, args[1].Integer());
}
